EKS Auto Mode で優先度の低い Pod を配置して、Pod が無くても常に一定数のノードが起動するようにする
EKS Auto Mode には下記メリットがあり、採用することでユーザー側の管理対象を大きく減らすことができます。
- ノード管理(スケーリング、パッチ管理)を AWS に移譲できる
- スケーリングはマネージドな形でインストールされた Karpenter が行う
- 必須コンポーネントが AWS 管理となっている
- VPC CNI
- CoreDNS
- AWS LoadBalancer Controller
- EBS CSI driver
- Pod Identity Agent
- ノード監視エージェント
一方で Karpenter 前提の仕組みとなるため、ノードのスケールアウト/スケールインが積極的に実行されることを意識して利用する必要があります。
特に Pod が存在しない Node はすぐスケールインされるので、細かい Job が継続的に発生するようなワークロードでは過剰にスケールインされる可能性があります。
Job 起動の度に EC2 を立ち上げる挙動になって、その分のオーバーヘッドが毎回発生する形になるわけです。
この辺りは Karpenter 入り EKS と GitLab Runner の組み合わせで利用する際の注意点として、スリーシェイクさんや Sky さんがわかりやすい資料を公開して下さっています。
この手法では、余剰な Pod をプロビジョニングしておくことで必要な Node 数を確保します。
余剰 Pod は優先度を低く設定しておくことで、必要な Pod がデプロイされたら追い出されるように設計できます。
こうすることで、Job 実行時に毎回 Node プロビジョニングが発生する状況を避けることができるわけです。
Cluster Autoscaler の場合ですが、EKS Workshop でも同様の手法が紹介されています。
GKE AutoPilot でも同様の課題があり、下記記事からこの手法で利用される余剰 Pod は Balloon Pod と呼ばれたりします。
管理対象を減らす目的で EKS Auto Mode を採用したものの、この辺りの挙動に苦しむ方がいるかもしれないと思ったので、今回 EKS Auto Mode で検証してみます。
ちなみに、この方法は現時点では良いだろうというだけなので、Karpenter の機能追加によってはより良い解決方法が生まれるかもしれません。
下記 issue も継続的して確認しておくと良いと思います。
やってみる
Auto Mode を有効化した EKS v1.31 を用意します。
この状態では Node が存在しません。
% kubectl get node
No resources found
ここで Job を実行してみます。
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl:5.34.0
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
resources:
requests:
cpu: 500m
memory: 1000Mi
restartPolicy: Never
backoffLimit: 4
上記マニフェストファイルは Kubernetes 公式ドキュメントから引っ張ってきた、π を 2000 桁まで計算して出力する Job に requests
の指定を追加しています。
Job 自体は 10 秒で終了する簡単なものですが、Node のプロビジョニングで 50 秒ほど要しています。
% kubectl describe pod
Name: pi-89m47
Namespace: default
Priority: 0
Service Account: default
Node: i-0740e2a4362e88675/10.0.101.77
Start Time: Mon, 06 Jan 2025 16:51:22 +0900
Labels: batch.kubernetes.io/controller-uid=3bb71e18-f3e6-4220-9851-f5334717d5a7
batch.kubernetes.io/job-name=pi
controller-uid=3bb71e18-f3e6-4220-9851-f5334717d5a7
job-name=pi
Annotations: <none>
Status: Succeeded
IP: 10.0.101.80
IPs:
IP: 10.0.101.80
Controlled By: Job/pi
Containers:
pi:
Container ID: containerd://62b527d712eb7210c13232af760f0532b6b925014d117008eec03ef68098bdfd
Image: perl:5.34.0
Image ID: docker.io/library/perl@sha256:2584f46a92d1042b25320131219e5832c5b3e75086dfaaff33e4fda7a9f47d99
Port: <none>
Host Port: <none>
Command:
perl
-Mbignum=bpi
-wle
print bpi(2000)
State: Terminated
Reason: Completed
Exit Code: 0
Started: Mon, 06 Jan 2025 16:51:50 +0900
Finished: Mon, 06 Jan 2025 16:51:54 +0900
Ready: False
Restart Count: 0
Requests:
cpu: 500m
memory: 1000Mi
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-nl2k6 (ro)
Conditions:
Type Status
PodReadyToStartContainers False
Initialized True
Ready False
ContainersReady False
PodScheduled True
Volumes:
kube-api-access-nl2k6:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Nominated 103s karpenter Pod should schedule on: nodeclaim/general-purpose-2bsv6
Warning FailedScheduling 88s (x5 over 104s) default-scheduler no nodes available to schedule pods
Normal Scheduled 78s default-scheduler Successfully assigned default/pi-89m47 to i-0740e2a4362e88675
Warning FailedCreatePodSandBox 78s kubelet Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "34c5f07273d27bc03be1e4ff6391a855dcf6237eea5a75dd032a5a4b5764bda9": plugin type="aws-cni" name="aws-cni" failed (add): add cmd: Error received from AddNetwork gRPC call: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp 127.0.0.1:50051: connect: connection refused"
Normal Pulling 65s kubelet Pulling image "perl:5.34.0"
Normal Pulled 51s kubelet Successfully pulled image "perl:5.34.0" in 13.9s (13.9s including waiting). Image size: 336374010 bytes.
Normal Created 51s kubelet Created container pi
Normal Started 51s kubelet Started container pi
これで問題無い場合も多いと思いますが、起動時間がシビアなケースもあるかと思います(例えば GitLab の CI であれば、push 後に CI 結果を確認できるまでの時間が開発体験に直結します)。
それでは、余剰に Pod をプロビジョニングする手法を試してみます。
まずは Default の PriorityClass を作成します。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: default
value: 0
preemptionPolicy: PreemptLowerPriority
globalDefault: true
description: "Default Priority class."
Kubernetes には Pod をスケジューリングできない時に既存 Pod を追い出す(プリエンプション)機能があります。
preemptionPolicy
が PreemptLowerPriority
になっている場合、自分より優先度の低い Pod を追い出してデプロイすることができます。
preemptionPolicy はデフォルトでは PreemptLowerPriority に設定されており、これが設定されている Pod は優先度の低い Pod をプリエンプトすることを許容します。これは既存のデフォルトの挙動です。 preemptionPolicy を Never に設定すると、これが設定された Pod はプリエンプトを行わないようになります。
https://kubernetes.io/ja/docs/concepts/scheduling-eviction/pod-priority-preemption/
次に余剰 Pod の PriorityClass を作成します。
こちらは、優先度を下げつつ、他 Pod を追い出さないように preemptionPolicy
を Never
に設定します。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: balloon
value: -10
preemptionPolicy: Never
globalDefault: false
description: "Balloon Pod priority."
PriorityClass 一覧を取得すると下記のようになります。
% kubectl get priorityclass
NAME VALUE GLOBAL-DEFAULT AGE
balloon -10 false 11s
default 0 true 18s
system-cluster-critical 2000000000 false 149m
system-node-critical 2000001000 false 149m
PriorityClass balloon を指定して、Pod を作成します。
起動さえしていればどんなコンテナでも良いので、無限に sleep させておきます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: balloon
spec:
replicas: 3
selector:
matchLabels:
pod: balloon
template:
metadata:
labels:
pod: balloon
spec:
priorityClassName: balloon
terminationGracePeriodSeconds: 0
containers:
- name: ubuntu
image: ubuntu
command: ["sleep"]
args: ["infinity"]
resources:
requests:
cpu: 500m
memory: 750Mi
この状態では c5a.large のノードが一つ起動され、ほぼ利用されている状態です。
先ほどと同様の Job を実行してみます。
利用可能な CPU のキャパシティが 500m も無いため、プリエンプションしなければ既存ノードにはデプロイできないはずです。
ただ、既存のインスタンス(i-059fc36710c53b69c)にスケジュールされて、2 秒程度でイメージの Pull が始まっています。
% kubectl describe pod pi-j5z96
Name: pi-j5z96
Namespace: default
Priority: 0
Priority Class Name: default
Service Account: default
Node: i-059fc36710c53b69c/10.0.100.201
Start Time: Mon, 06 Jan 2025 17:09:01 +0900
Labels: batch.kubernetes.io/controller-uid=93c079d1-1a45-47ae-adf0-5346122d3983
batch.kubernetes.io/job-name=pi
controller-uid=93c079d1-1a45-47ae-adf0-5346122d3983
job-name=pi
Annotations: <none>
Status: Running
IP: 10.0.100.51
IPs:
IP: 10.0.100.51
Controlled By: Job/pi
Containers:
pi:
Container ID: containerd://abc0c6a63d5c11d518018025c56fb14b245543cb0ce80d16f74a1814a7ff3a92
Image: perl:5.34.0
Image ID: docker.io/library/perl@sha256:2584f46a92d1042b25320131219e5832c5b3e75086dfaaff33e4fda7a9f47d99
Port: <none>
Host Port: <none>
Command:
perl
-Mbignum=bpi
-wle
print bpi(2000)
State: Running
Started: Mon, 06 Jan 2025 17:09:16 +0900
Ready: True
Restart Count: 0
Requests:
cpu: 500m
memory: 1000Mi
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-n58kw (ro)
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-n58kw:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 19s default-scheduler 0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory.
Normal Scheduled 17s default-scheduler Successfully assigned default/pi-j5z96 to i-059fc36710c53b69c
Normal Pulling 17s kubelet Pulling image "perl:5.34.0"
Normal Pulled 2s kubelet Successfully pulled image "perl:5.34.0" in 14.383s (14.383s including waiting). Image size: 336374010 bytes.
Normal Created 2s kubelet Created container pi
Normal Started 2s kubelet Started container pi
無事、EKS Auto Mode でも Balloon Pod のメリットを享受できていますね!
こちらも Sky さんの記事 で既に書かれている内容ですが、スケジュールベースで 0 スケール可能な KEDA と組み合わせれば、日中帯のみ Node をオーバープロビジョニングするようなことも可能です。
例えば下記のような定義を行うことで、8:30 に Node を起動して、18:00 過ぎに Node を削除するようなことが可能です。
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: balloon
spec:
cooldownPeriod: 300
minReplicaCount: 0
scaleTargetRef:
name: balloon
triggers:
- metadata:
desiredReplicas: "3"
start: 30 8 * * *
end: 00 18 * * *
timezone: Asia/Tokyo
type: cron
※ あくまで事前に用意する Node の話なので、Pod が起動されれば夜間帯でも Node が立ち上がります。
KEDA は Helm で簡単にインストールできるので是非試してみて下さい。
まとめ
EKS Auto Mode で優先度の低い Pod を作成して、常時 Node が起動されるようにしてみました。
GitLab Runnner のみを EKS で動かす時のような、常時細かい Job が作成され続ける環境では有用なテクニックでは無いでしょうか。
この記事がどなたかの参考になれば幸いです。